// Copyright 2018-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/accounts_test.go and server/dirstore_test.go in the NATS server Go source.

using Shouldly;
using Xunit;

namespace ZB.MOM.NatsNet.Server.Tests;

[Collection("AccountTests")]
public sealed partial class AccountTests
{
    // =========================================================================
    // Account Basic Tests
    // =========================================================================

    // Test 1
    [Fact]
    public void NewAccount_SetsNameAndUnlimitedLimits()
    {
        var acc = Account.NewAccount("foo");

        acc.Name.ShouldBe("foo");
        acc.MaxConnections.ShouldBe(-1);
        acc.MaxLeafNodes.ShouldBe(-1);
    }

    // Test 2
    [Fact]
    public void ToString_ReturnsName()
    {
        var acc = Account.NewAccount("myaccount");

        acc.ToString().ShouldBe(acc.Name);
    }

    // Test 3
    [Fact]
    public void IsExpired_InitiallyFalse()
    {
        var acc = Account.NewAccount("foo");

        acc.IsExpired().ShouldBeFalse();
    }

    // Test 4
    [Fact]
    public void IsClaimAccount_NoJwt_ReturnsFalse()
    {
        var acc = Account.NewAccount("foo");
        // ClaimJwt defaults to empty string
        acc.IsClaimAccount().ShouldBeFalse();
    }

    // Test 5
    [Fact]
    public void NumConnections_Initial_IsZero()
    {
        var acc = Account.NewAccount("foo");

        acc.NumConnections().ShouldBe(0);
    }

    // Test 6
    [Fact]
    public void GetName_ReturnsName()
    {
        var acc = Account.NewAccount("thread-safe-name");

        acc.GetName().ShouldBe("thread-safe-name");
    }

    // =========================================================================
    // Subject Mapping Tests
    // =========================================================================

    // Test 7
    [Fact]
    public void AddMapping_ValidSubject_Succeeds()
    {
        var acc = Account.NewAccount("foo");

        var err = acc.AddMapping("foo", "bar");

        err.ShouldBeNull();
    }

    // Test 8
    [Fact]
    public void AddMapping_InvalidSubject_ReturnsError()
    {
        var acc = Account.NewAccount("foo");

        var err = acc.AddMapping("foo..bar", "x");

        err.ShouldNotBeNull();
    }

    // Test 9
    [Fact]
    public void RemoveMapping_ExistingMapping_ReturnsTrue()
    {
        var acc = Account.NewAccount("foo");
        acc.AddMapping("foo", "bar").ShouldBeNull();

        var removed = acc.RemoveMapping("foo");

        removed.ShouldBeTrue();
    }

    // Test 10
    [Fact]
    public void RemoveMapping_NonExistentMapping_ReturnsFalse()
    {
        var acc = Account.NewAccount("foo");

        var removed = acc.RemoveMapping("nonexistent");

        removed.ShouldBeFalse();
    }

    // Test 11
    [Fact]
    public void HasMappings_AfterAdd_ReturnsTrue()
    {
        var acc = Account.NewAccount("foo");
        acc.AddMapping("foo", "bar").ShouldBeNull();

        acc.HasMappings().ShouldBeTrue();
    }

    // Test 12
    [Fact]
    public void HasMappings_AfterRemove_ReturnsFalse()
    {
        var acc = Account.NewAccount("foo");
        acc.AddMapping("foo", "bar").ShouldBeNull();
        acc.RemoveMapping("foo");

        acc.HasMappings().ShouldBeFalse();
    }

    // Test 13
    [Fact]
    public void SelectMappedSubject_NoMapping_ReturnsFalse()
    {
        var acc = Account.NewAccount("foo");

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

        mapped.ShouldBeFalse();
        dest.ShouldBe("foo");
    }

    // Test 14
    [Fact]
    public void SelectMappedSubject_SimpleMapping_ReturnsMappedDest()
    {
        var acc = Account.NewAccount("foo");
        acc.AddMapping("foo", "bar").ShouldBeNull();

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

        mapped.ShouldBeTrue();
        dest.ShouldBe("bar");
    }

    // Test 15
    [Fact]
    public void AddWeightedMappings_DuplicateDest_ReturnsError()
    {
        var acc = Account.NewAccount("foo");

        var err = acc.AddWeightedMappings("src",
            MapDest.New("dest1", 50),
            MapDest.New("dest1", 50)); // duplicate subject

        err.ShouldNotBeNull();
    }

    // Test 16
    [Fact]
    public void AddWeightedMappings_WeightOver100_ReturnsError()
    {
        var acc = Account.NewAccount("foo");

        var err = acc.AddWeightedMappings("src",
            MapDest.New("dest1", 101)); // weight exceeds 100

        err.ShouldNotBeNull();
    }

    // Test 17
    [Fact]
    public void AddWeightedMappings_TotalWeightOver100_ReturnsError()
    {
        var acc = Account.NewAccount("foo");

        var err = acc.AddWeightedMappings("src",
            MapDest.New("dest1", 80),
            MapDest.New("dest2", 80)); // total = 160

        err.ShouldNotBeNull();
    }

    // =========================================================================
    // Connection Counting Tests
    // =========================================================================

    // Test 18
    [Fact]
    public void NumLeafNodes_Initial_IsZero()
    {
        var acc = Account.NewAccount("foo");

        acc.NumLeafNodes().ShouldBe(0);
    }

    // Test 19
    [Fact]
    public void MaxTotalConnectionsReached_UnlimitedAccount_ReturnsFalse()
    {
        var acc = Account.NewAccount("foo");
        // MaxConnections is -1 (unlimited) by default

        acc.MaxTotalConnectionsReached().ShouldBeFalse();
    }

    // Test 20
    [Fact]
    public void MaxTotalLeafNodesReached_UnlimitedAccount_ReturnsFalse()
    {
        var acc = Account.NewAccount("foo");
        // MaxLeafNodes is -1 (unlimited) by default

        acc.MaxTotalLeafNodesReached().ShouldBeFalse();
    }

    // =========================================================================
    // Export Service Tests
    // =========================================================================

    // Test 21
    [Fact]
    public void IsExportService_NoExports_ReturnsFalse()
    {
        var acc = Account.NewAccount("foo");

        acc.IsExportService("my.service").ShouldBeFalse();
    }

    // Test 22
    [Fact]
    public void IsExportServiceTracking_NoExports_ReturnsFalse()
    {
        var acc = Account.NewAccount("foo");

        acc.IsExportServiceTracking("my.service").ShouldBeFalse();
    }
}

// =========================================================================
// DirJwtStore Tests
// =========================================================================

[Collection("AccountTests")]
public sealed class DirJwtStoreTests : IDisposable
{
    private readonly List<string> _tempDirs = [];

    private string MakeTempDir()
    {
        var dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
        Directory.CreateDirectory(dir);
        _tempDirs.Add(dir);
        return dir;
    }

    public void Dispose()
    {
        foreach (var dir in _tempDirs)
        {
            try { Directory.Delete(dir, true); } catch { /* best effort */ }
        }
    }

    // Test 23
    [Fact]
    public void DirJwtStore_WriteAndRead_Succeeds()
    {
        var dir = MakeTempDir();
        using var store = DirJwtStore.NewDirJwtStore(dir, shard: false, create: false);

        const string key = "AAAAAAAAAA"; // minimum 2-char key
        const string jwt = "header.payload.signature";

        store.SaveAcc(key, jwt);
        var loaded = store.LoadAcc(key);

        loaded.ShouldBe(jwt);
    }

    // Test 24
    [Fact]
    public void DirJwtStore_ShardedWriteAndRead_Succeeds()
    {
        var dir = MakeTempDir();
        using var store = DirJwtStore.NewDirJwtStore(dir, shard: true, create: false);

        var keys = new[] { "ACCTKEY001", "ACCTKEY002", "ACCTKEY003" };
        foreach (var k in keys)
        {
            store.SaveAcc(k, $"jwt.for.{k}");
        }

        foreach (var k in keys)
        {
            store.LoadAcc(k).ShouldBe($"jwt.for.{k}");
        }
    }

    // Test 25
    [Fact]
    public void DirJwtStore_EmptyKey_ReturnsError()
    {
        var dir = MakeTempDir();
        using var store = DirJwtStore.NewDirJwtStore(dir, shard: false, create: false);

        // LoadAcc with key shorter than 2 chars should throw
        Should.Throw<Exception>(() => store.LoadAcc(""));

        // SaveAcc with key shorter than 2 chars should throw
        Should.Throw<Exception>(() => store.SaveAcc("", "some.jwt"));
    }

    // Test 26
    [Fact]
    public void DirJwtStore_MissingKey_ReturnsError()
    {
        var dir = MakeTempDir();
        using var store = DirJwtStore.NewDirJwtStore(dir, shard: false, create: false);

        Should.Throw<FileNotFoundException>(() => store.LoadAcc("NONEXISTENT_KEY"));
    }

    // Test 27
    [Fact]
    public void DirJwtStore_Pack_ContainsSavedJwts()
    {
        var dir = MakeTempDir();
        using var store = DirJwtStore.NewDirJwtStore(dir, shard: false, create: false);

        store.SaveAcc("ACCTKEYAAA", "jwt1.data.sig");
        store.SaveAcc("ACCTKEYBBB", "jwt2.data.sig");

        var packed = store.Pack(-1);

        packed.ShouldContain("ACCTKEYAAA|jwt1.data.sig");
        packed.ShouldContain("ACCTKEYBBB|jwt2.data.sig");
    }

    // Test 28
    [Fact]
    public void DirJwtStore_Merge_AddsNewEntries()
    {
        var dir = MakeTempDir();
        using var store = DirJwtStore.NewDirJwtStore(dir, shard: false, create: false);

        // Pack format: key|jwt lines separated by newline
        var packData = "ACCTKEYMERGE|merged.jwt.value";
        store.Merge(packData);

        var loaded = store.LoadAcc("ACCTKEYMERGE");
        loaded.ShouldBe("merged.jwt.value");
    }

    // Test 29
    [Fact]
    public void DirJwtStore_ReadOnly_Prevents_Write()
    {
        var dir = MakeTempDir();
        // Write a file first so the dir is valid
        var writeable = DirJwtStore.NewDirJwtStore(dir, shard: false, create: false);
        writeable.SaveAcc("ACCTKEYRO", "original.jwt");
        writeable.Dispose();

        // Open as immutable
        using var readOnly = DirJwtStore.NewImmutableDirJwtStore(dir, shard: false);

        readOnly.IsReadOnly().ShouldBeTrue();
        Should.Throw<InvalidOperationException>(() => readOnly.SaveAcc("ACCTKEYRO", "new.jwt"));
    }
}

// =========================================================================
// MemoryAccountResolver Tests
// =========================================================================

[Collection("AccountTests")]
public sealed class MemoryAccountResolverTests
{
    // Test 30
    [Fact]
    public async Task MemoryAccountResolver_StoreAndFetch_Roundtrip()
    {
        var resolver = new MemoryAccountResolver();
        const string key = "MYACCOUNTKEY";
        const string jwt = "header.payload.sig";

        await resolver.StoreAsync(key, jwt);
        var fetched = await resolver.FetchAsync(key);

        fetched.ShouldBe(jwt);
    }

    // Test 31
    [Fact]
    public async Task MemoryAccountResolver_Fetch_MissingKey_Throws()
    {
        var resolver = new MemoryAccountResolver();

        await Should.ThrowAsync<InvalidOperationException>(
            () => resolver.FetchAsync("DOESNOTEXIST"));
    }

    // Test 32
    [Fact]
    public void MemoryAccountResolver_IsReadOnly_ReturnsFalse()
    {
        var resolver = new MemoryAccountResolver();

        resolver.IsReadOnly().ShouldBeFalse();
    }
}

// =========================================================================
// UrlAccountResolver Tests
// =========================================================================

[Collection("AccountTests")]
public sealed class UrlAccountResolverTests
{
    // Test 33
    [Fact]
    public void UrlAccountResolver_NormalizesTrailingSlash()
    {
        // Two constructors: one with slash, one without.
        // We verify construction doesn't throw and the resolver is usable.
        // (We cannot inspect _url directly since it's private, but we can
        // infer correctness via IsReadOnly and lack of constructor exception.)
        var resolverNoSlash = new UrlAccountResolver("http://localhost:9090");
        var resolverWithSlash = new UrlAccountResolver("http://localhost:9090/");

        // Both should construct without error and have the same observable behaviour.
        resolverNoSlash.IsReadOnly().ShouldBeTrue();
        resolverWithSlash.IsReadOnly().ShouldBeTrue();
    }

    // Test 34
    [Fact]
    public void UrlAccountResolver_IsReadOnly_ReturnsTrue()
    {
        var resolver = new UrlAccountResolver("http://localhost:9090");

        resolver.IsReadOnly().ShouldBeTrue();
    }
}
