using Microsoft.Extensions.Configuration;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Configuration;

namespace ZB.MOM.WW.Secrets.Tests.Configuration;

public sealed class SecretReferenceExpanderTests
{
    private sealed class FakeSecretResolver : ISecretResolver
    {
        private readonly Dictionary<string, string?> _values;

        public FakeSecretResolver(Dictionary<string, string?> values) => _values = values;

        public Task<string?> GetAsync(SecretName name, CancellationToken ct) =>
            Task.FromResult(_values.TryGetValue(name.Value, out string? v) ? v : null);
    }

    private static SecretReferenceExpander Expander(params (string Name, string? Value)[] secrets)
    {
        var dict = new Dictionary<string, string?>();
        foreach ((string name, string? value) in secrets)
        {
            dict[new SecretName(name).Value] = value;
        }

        return new SecretReferenceExpander(new FakeSecretResolver(dict));
    }

    [Fact]
    public async Task ExpandAsync_SingleToken()
    {
        SecretReferenceExpander expander = Expander(("sql/foo", "hunter2"));

        string result = await expander.ExpandAsync("Password=${secret:sql/foo}", CancellationToken.None);

        Assert.Equal("Password=hunter2", result);
    }

    [Fact]
    public async Task ExpandAsync_MultipleTokens()
    {
        SecretReferenceExpander expander = Expander(("sql/foo", "hunter2"), ("sql/bar", "swordfish"));

        string result = await expander.ExpandAsync(
            "u=${secret:sql/foo};p=${secret:sql/bar}", CancellationToken.None);

        Assert.Equal("u=hunter2;p=swordfish", result);
    }

    [Fact]
    public async Task ExpandAsync_NoToken_Unchanged()
    {
        SecretReferenceExpander expander = Expander(("sql/foo", "hunter2"));

        string result = await expander.ExpandAsync("no tokens here", CancellationToken.None);

        Assert.Equal("no tokens here", result);
    }

    [Fact]
    public async Task ExpandAsync_MissingSecret_Throws()
    {
        SecretReferenceExpander expander = Expander();

        SecretNotFoundException ex = await Assert.ThrowsAsync<SecretNotFoundException>(
            () => expander.ExpandAsync("Password=${secret:sql/absent}", CancellationToken.None));

        Assert.Contains("sql/absent", ex.Message);
        Assert.Contains("${secret:sql/absent}", ex.Message);
    }

    [Fact]
    public async Task ExpandConfigurationAsync_RewritesMatchingKeys()
    {
        SecretReferenceExpander expander = Expander(("sql/foo", "hunter2"));
        IConfigurationRoot config = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["ConnectionStrings:Db"] = "Server=.;Password=${secret:sql/foo}",
                ["Plain:Value"] = "unchanged",
            })
            .Build();

        await expander.ExpandConfigurationAsync(config, CancellationToken.None);

        Assert.Contains("hunter2", config["ConnectionStrings:Db"]);
        Assert.DoesNotContain("${secret:", config["ConnectionStrings:Db"]);
        Assert.Equal("unchanged", config["Plain:Value"]);
    }

    [Fact]
    public async Task ExpandConfigurationAsync_MissingSecret_FailsClosed()
    {
        SecretReferenceExpander expander = Expander();
        IConfigurationRoot config = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["ConnectionStrings:Db"] = "Server=.;Password=${secret:sql/absent}",
            })
            .Build();

        await Assert.ThrowsAsync<SecretNotFoundException>(
            () => expander.ExpandConfigurationAsync(config, CancellationToken.None));
    }

    [Fact]
    public async Task ExpandConfigurationAsync_SkipsCommentKeys()
    {
        // A "_comment" key may document the ${secret:...} syntax with a literal (even malformed or
        // not-yet-seeded) example token; it must be left untouched — never resolved, never throwing —
        // while a real sibling key still expands. Regression: an unseeded/ellipsis example in a comment
        // previously crashed boot (SecretName rejects '...').
        SecretReferenceExpander expander = Expander(("sql/foo", "hunter2"));
        IConfigurationRoot config = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["_secretsComment"] = "e.g. Password=${secret:...} or ${secret:sql/not-seeded}",
                ["Nested:_comment"] = "also skipped: ${secret:sql/also-absent}",
                ["ConnectionStrings:Db"] = "Server=.;Password=${secret:sql/foo}",
            })
            .Build();

        await expander.ExpandConfigurationAsync(config, CancellationToken.None);

        // Comment keys untouched (literal token preserved, no throw despite absent/malformed names).
        Assert.Equal("e.g. Password=${secret:...} or ${secret:sql/not-seeded}", config["_secretsComment"]);
        Assert.Contains("${secret:sql/also-absent}", config["Nested:_comment"]);
        // Real key still expanded.
        Assert.Equal("Server=.;Password=hunter2", config["ConnectionStrings:Db"]);
    }
}
