using System.Text;
using System.Text.Json;
using NATS.Client.Core;
using NATS.E2E.Tests.Infrastructure;
using NATS.NKeys;

namespace NATS.E2E.Tests;

public class AdvancedTests
{
    [Fact]
    public async Task ConfigFile_FullConfig_ServerStartsAndAcceptsConnections()
    {
        var config = """
            server_name: e2e-config-test
            max_payload: 2048
            max_connections: 100
            """;

        await using var server = NatsServerProcess.WithConfig(config);
        await server.StartAsync();

        await using var client = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{server.Port}" });
        await client.ConnectAsync();
        await client.PingAsync();

        client.ConnectionState.ShouldBe(NatsConnectionState.Open);
        client.ServerInfo.ShouldNotBeNull();
        client.ServerInfo!.MaxPayload.ShouldBe(2048);
    }

    [Fact]
    public async Task MaxConnections_ExceedsLimit_Rejected()
    {
        var config = """
            max_connections: 2
            """;

        await using var server = NatsServerProcess.WithConfig(config);
        await server.StartAsync();

        var url = $"nats://127.0.0.1:{server.Port}";

        await using var c1 = new NatsConnection(new NatsOpts { Url = url });
        await using var c2 = new NatsConnection(new NatsOpts { Url = url });

        await c1.ConnectAsync();
        await c1.PingAsync();

        await c2.ConnectAsync();
        await c2.PingAsync();

        await using var c3 = new NatsConnection(new NatsOpts
        {
            Url = url,
            MaxReconnectRetry = 0,
        });

        var ex = await Should.ThrowAsync<Exception>(async () =>
        {
            await c3.ConnectAsync();
            await c3.PingAsync();
        });

        ex.ShouldNotBeNull();
    }

    [Fact]
    public async Task SystemEvents_ClientConnect_EventPublished()
    {
        var config = """
            accounts {
                SYS {
                    users = [{ user: "sys", password: "sys" }]
                }
                APP {
                    users = [{ user: "app", password: "app" }]
                }
            }
            system_account: SYS
            """;

        await using var server = NatsServerProcess.WithConfig(config);
        await server.StartAsync();

        var url = $"nats://127.0.0.1:{server.Port}";

        await using var sysClient = new NatsConnection(new NatsOpts
        {
            Url = url,
            AuthOpts = new NatsAuthOpts { Username = "sys", Password = "sys" },
        });
        await sysClient.ConnectAsync();

        await using var subscription = await sysClient.SubscribeCoreAsync<string>("$SYS.ACCOUNT.*.CONNECT");
        await sysClient.PingAsync();

        await using var appClient = new NatsConnection(new NatsOpts
        {
            Url = url,
            AuthOpts = new NatsAuthOpts { Username = "app", Password = "app" },
        });
        await appClient.ConnectAsync();
        await appClient.PingAsync();

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
        var msg = await subscription.Msgs.ReadAsync(cts.Token);
        msg.Subject.ShouldContain("CONNECT");
    }

    [Fact]
    public async Task AccountImportExport_CrossAccountServiceCall()
    {
        var config = """
            accounts {
                PROVIDER {
                    users = [{ user: "provider", password: "prov" }]
                    exports = [
                        { service: "svc.echo" }
                    ]
                }
                CONSUMER {
                    users = [{ user: "consumer", password: "cons" }]
                    imports = [
                        { service: { account: PROVIDER, subject: "svc.echo" } }
                    ]
                }
            }
            """;

        await using var server = NatsServerProcess.WithConfig(config);
        await server.StartAsync();

        var url = $"nats://127.0.0.1:{server.Port}";

        await using var provider = new NatsConnection(new NatsOpts
        {
            Url = url,
            AuthOpts = new NatsAuthOpts { Username = "provider", Password = "prov" },
        });
        await provider.ConnectAsync();

        await using var svcSub = await provider.SubscribeCoreAsync<string>("svc.echo");
        await provider.PingAsync();

        var responderTask = Task.Run(async () =>
        {
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
            var msg = await svcSub.Msgs.ReadAsync(cts.Token);
            await provider.PublishAsync(msg.ReplyTo!, $"echo: {msg.Data}");
        });

        await using var consumer = new NatsConnection(new NatsOpts
        {
            Url = url,
            AuthOpts = new NatsAuthOpts { Username = "consumer", Password = "cons" },
        });
        await consumer.ConnectAsync();

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
        var reply = await consumer.RequestAsync<string, string>("svc.echo", "hello", cancellationToken: cts.Token);

        reply.Data.ShouldBe("echo: hello");

        await responderTask;
    }

    [Fact]
    public async Task ServiceLatency_CrossAccountCall_LatencyMessagePublished()
    {
        var config = """
            accounts {
                SYS {
                    users = [{ user: "sys", password: "sys" }]
                }
                PROVIDER {
                    users = [{ user: "provider", password: "prov" }]
                    exports = [
                        { service: "svc.echo", latency: "latency.svc.echo" }
                    ]
                }
                CONSUMER {
                    users = [{ user: "consumer", password: "cons" }]
                    imports = [
                        { service: { account: PROVIDER, subject: "svc.echo" } }
                    ]
                }
            }
            system_account: SYS
            """;

        await using var server = NatsServerProcess.WithConfig(config);
        await server.StartAsync();

        var url = $"nats://127.0.0.1:{server.Port}";

        // System account client subscribes to latency events
        await using var sysClient = new NatsConnection(new NatsOpts
        {
            Url = url,
            AuthOpts = new NatsAuthOpts { Username = "sys", Password = "sys" },
        });
        await sysClient.ConnectAsync();

        await using var latencySub = await sysClient.SubscribeCoreAsync<string>("latency.svc.echo");
        await sysClient.PingAsync();

        // Provider sets up the echo responder
        await using var provider = new NatsConnection(new NatsOpts
        {
            Url = url,
            AuthOpts = new NatsAuthOpts { Username = "provider", Password = "prov" },
        });
        await provider.ConnectAsync();

        await using var svcSub = await provider.SubscribeCoreAsync<string>("svc.echo");
        await provider.PingAsync();

        var responderTask = Task.Run(async () =>
        {
            using var cts2 = new CancellationTokenSource(TimeSpan.FromSeconds(10));
            var msg = await svcSub.Msgs.ReadAsync(cts2.Token);
            await provider.PublishAsync(msg.ReplyTo!, $"echo: {msg.Data}");
        });

        // Consumer makes a cross-account service call
        await using var consumer = new NatsConnection(new NatsOpts
        {
            Url = url,
            AuthOpts = new NatsAuthOpts { Username = "consumer", Password = "cons" },
        });
        await consumer.ConnectAsync();

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
        var reply = await consumer.RequestAsync<string, string>("svc.echo", "hello", cancellationToken: cts.Token);
        reply.Data.ShouldBe("echo: hello");

        await responderTask;

        // Verify latency message was published to the system account
        var latencyMsg = await latencySub.Msgs.ReadAsync(cts.Token);
        latencyMsg.Subject.ShouldBe("latency.svc.echo");
        latencyMsg.Data.ShouldNotBeNull();
        latencyMsg.Data!.ShouldContain("service_latency");
    }

    [Fact]
    public async Task SubjectTransforms_MappedSubject_ReceivedOnTarget()
    {
        var config = """
            mappings {
                "e2e.src": "e2e.dest"
            }
            """;

        await using var server = NatsServerProcess.WithConfig(config);
        await server.StartAsync();

        var url = $"nats://127.0.0.1:{server.Port}";

        await using var client = new NatsConnection(new NatsOpts { Url = url });
        await client.ConnectAsync();

        // Subscribe to the destination subject
        await using var sub = await client.SubscribeCoreAsync<string>("e2e.dest");
        await client.PingAsync();

        // Publish to the source subject — should be transformed to destination
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        await client.PublishAsync("e2e.src", "mapped-payload", cancellationToken: cts.Token);
        await client.PingAsync(cts.Token);

        var msg = await sub.Msgs.ReadAsync(cts.Token);
        msg.Data.ShouldBe("mapped-payload");
    }

    [Fact]
    public async Task JwtAuth_ValidJwt_Connects()
    {
        // Generate operator, account, and user NKey pairs
        using var operatorKp = KeyPair.CreatePair(PrefixByte.Operator);
        using var accountKp = KeyPair.CreatePair(PrefixByte.Account);
        using var userKp = KeyPair.CreatePair(PrefixByte.User);

        var operatorPub = operatorKp.GetPublicKey();
        var accountPub = accountKp.GetPublicKey();
        var userPub = userKp.GetPublicKey();

        // Build account JWT (signed by operator)
        var accountJwt = BuildJwt(new
        {
            sub = accountPub,
            iss = operatorPub,
            iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
            nats = new { type = "account", version = 2 },
        }, operatorKp);

        // Build user JWT as bearer token (signed by account, no nonce needed)
        var userJwt = BuildJwt(new
        {
            sub = userPub,
            iss = accountPub,
            iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
            nats = new { type = "user", version = 2, bearer_token = true, issuer_account = accountPub },
        }, accountKp);

        var config = $$"""
            trusted_keys: "{{operatorPub}}"
            resolver: MEMORY
            resolver_preload: {
                {{accountPub}}: "{{accountJwt}}"
            }
            """;

        await using var server = NatsServerProcess.WithConfig(config);
        await server.StartAsync();

        var url = $"nats://127.0.0.1:{server.Port}";

        await using var client = new NatsConnection(new NatsOpts
        {
            Url = url,
            AuthOpts = new NatsAuthOpts { Jwt = userJwt },
        });

        await client.ConnectAsync();
        await client.PingAsync();
        client.ConnectionState.ShouldBe(NatsConnectionState.Open);
    }

    /// <summary>
    /// Builds a signed NATS JWT using the given payload and NKey pair.
    /// Wire format: base64url(header).base64url(payload).base64url(ed25519-signature).
    /// </summary>
    private static string BuildJwt(object payload, KeyPair signingKp)
    {
        var header = """{"typ":"jwt","alg":"ed25519-nkey"}""";
        var payloadJson = JsonSerializer.Serialize(payload);

        var headerB64 = Base64UrlEncode(Encoding.UTF8.GetBytes(header));
        var payloadB64 = Base64UrlEncode(Encoding.UTF8.GetBytes(payloadJson));

        var signingInput = Encoding.UTF8.GetBytes($"{headerB64}.{payloadB64}");
        var sig = new byte[64];
        signingKp.Sign(signingInput, sig);

        return $"{headerB64}.{payloadB64}.{Base64UrlEncode(sig)}";
    }

    private static string Base64UrlEncode(byte[] data)
        => Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
