{%- import "cloudevents.jinja.include" as cloudEvents -%}
{%- import "amqp.jinja.include" as amqp -%}
{%- import "util.jinja.include" as util -%}
{{ util.CommonFileHeader() }}
{%- set messagegroups = root.messagegroups %}

#nullable enable

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
using Microsoft.Extensions.Logging;
using Amqp;
using Amqp.Framing;
using Amqp.Types;
using Testcontainers.ActiveMq;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
using CloudNative.CloudEvents;

using {{ project_name | pascal }};

namespace {{ project_name | pascal }}.Test
{
    // Base fixture class for AMQP broker setup
    public abstract class AmqpBrokerFixture : IAsyncLifetime
    {
        
        protected const string brokerXml = @"<?xml version='1.0'?>
            <configuration xmlns=""urn:activemq""
                        xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance""
                        xmlns:xi=""http://www.w3.org/2001/XInclude""
                        xsi:schemaLocation=""urn:activemq /schema/artemis-configuration.xsd"">

            <core xmlns=""urn:activemq:core"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance""
                    xsi:schemaLocation=""urn:activemq:core "">
                <name>0.0.0.0</name>
                <persistence-enabled>false</persistence-enabled>
                <journal-type>NIO</journal-type>
                <!-- should the broker detect dead locks and other issues -->
                <critical-analyzer>true</critical-analyzer>
                <critical-analyzer-timeout>120000</critical-analyzer-timeout>
                <critical-analyzer-check-period>60000</critical-analyzer-check-period>
                <critical-analyzer-policy>HALT</critical-analyzer-policy>
                <page-sync-timeout>44000</page-sync-timeout>
                <acceptors>
                    <!-- Acceptor for every supported protocol -->
                    <acceptor name=""artemis"">tcp://0.0.0.0:5660?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;amqpMinLargeMessageSize=102400;protocols=CORE,AMQP,STOMP,HORNETQ,MQTT,OPENWIRE;useEpoll=true;amqpCredits=1000;amqpLowCredits=300;amqpDuplicateDetection=true</acceptor>
                    <acceptor name=""amqp"">tcp://0.0.0.0:5672?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;protocols=AMQP;useEpoll=true;saslMechanisms=EXTERNAL,PLAIN,ANONYMOUS</acceptor>
                </acceptors>
                <broker-connections>
                    <amqp-connection uri=""tcp://localhost:5672"" name=""receiver"" retry-interval=""100"">
                        <!-- This will create one receiver for every queue matching this address expression -->
                        <receiver address-match=""#""/>
                    </amqp-connection>
                </broker-connections>
                <security-settings>
                    <security-setting match=""#"">
                        <permission type=""createNonDurableQueue"" roles=""guest""/>
                        <permission type=""deleteNonDurableQueue"" roles=""guest""/>
                        <permission type=""createDurableQueue"" roles=""guest""/>
                        <permission type=""deleteDurableQueue"" roles=""guest""/>
                        <permission type=""createAddress"" roles=""guest""/>
                        <permission type=""deleteAddress"" roles=""guest""/>
                        <permission type=""consume"" roles=""guest""/>
                        <permission type=""browse"" roles=""guest""/>
                        <permission type=""send"" roles=""guest""/>
                        <permission type=""manage"" roles=""guest""/>
                    </security-setting>
                </security-settings>
                <address-settings>
                    <!-- if you define auto-create on certain queues, management has to be auto-create -->
                    <address-setting match=""activemq.management#"">
                        <dead-letter-address>DLQ</dead-letter-address>
                        <expiry-address>ExpiryQueue</expiry-address>
                        <redelivery-delay>0</redelivery-delay>
                        <!-- with -1 only the global-max-size is in use for limiting -->
                        <max-size-bytes>-1</max-size-bytes>
                        <message-counter-history-day-limit>10</message-counter-history-day-limit>
                        <address-full-policy>PAGE</address-full-policy>
                        <auto-create-queues>true</auto-create-queues>
                        <auto-create-addresses>true</auto-create-addresses>
                    </address-setting>
                    <!--default for catch all-->
                    <address-setting match=""#"">
                        <dead-letter-address>DLQ</dead-letter-address>
                        <expiry-address>ExpiryQueue</expiry-address>
                        <redelivery-delay>0</redelivery-delay>
                        <!-- with -1 only the global-max-size is in use for limiting -->
                        <max-size-bytes>-1</max-size-bytes>
                        <message-counter-history-day-limit>10</message-counter-history-day-limit>
                        <address-full-policy>PAGE</address-full-policy>
                        <auto-create-queues>true</auto-create-queues>
                        <auto-create-addresses>true</auto-create-addresses>
                    </address-setting>
                </address-settings>
                <addresses>
                    <address name=""exampleQueue"">
                        <anycast>
                        <queue name=""exampleQueue"" />
                        </anycast>
                    </address>
                </addresses>
            </core>
            </configuration>";

        public ArtemisContainer? ArtemisContainerInstance { get; protected set; }
        public IContainer? RabbitMqContainerInstance { get; protected set; }
        public Uri? BrokerUrl { get; protected set; }
        protected ILoggerFactory _loggerFactory;
        protected ILogger _logger;

        public ILoggerFactory GetLoggerFactory()
        {
            return _loggerFactory;
        }

        public string QueueName 
        {
            get
            {
                return "exampleQueue";
            }
        }

        public AmqpBrokerFixture()
        {
            _loggerFactory = LoggerFactory.Create(builder =>
            {
                builder.AddDebug().AddConsole();
            });
            _logger = _loggerFactory.CreateLogger<AmqpBrokerFixture>();
        }

        public abstract Task InitializeAsync();
        
        public abstract Task DisposeAsync();
    }

    // Fixture for ActiveMQ Artemis
    public class ArtemisFixture : AmqpBrokerFixture
    {
        public override async Task InitializeAsync()
        {
            try
            {
                _logger.LogDebug("Initializing Artemis test fixture.");
                var configFilePath = Path.GetTempFileName();
                File.WriteAllText(configFilePath, brokerXml);

                Amqp.Trace.TraceLevel = Amqp.TraceLevel.Frame;
                Amqp.Trace.TraceListener = (l, f, a) => System.Diagnostics.Debug.WriteLine(f, a);
                
                ArtemisContainerInstance = new ArtemisBuilder()
                        .WithBindMount(configFilePath, "/var/lib/artemis/etc-override/broker.xml")
                        .WithPortBinding(5672, true)
                        .WithUsername("guest")
                        .WithPassword("guest")
                        .Build();

                await ArtemisContainerInstance.StartAsync();
                _logger.LogDebug("Artemis container started.");
                BrokerUrl = new UriBuilder(ArtemisContainerInstance.GetBrokerAddress()){ Scheme = "amqp"}.Uri;
                _logger.LogDebug($"Artemis broker URL: {BrokerUrl}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "An error occurred during InitializeAsync");
                throw;
            }
        }

        public override async Task DisposeAsync()
        {
            try
            {
                _logger.LogDebug("Disposing Artemis test fixture.");
                
                if (ArtemisContainerInstance != null)
                {
                    await ArtemisContainerInstance.StopAsync();
                    _logger.LogDebug("Artemis container stopped.");
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "An error occurred during DisposeAsync");
            }
        }
    }

    // Fixture for RabbitMQ 3.x (with AMQP 1.0 plugin)
    public class RabbitMq3Fixture : AmqpBrokerFixture
    {
        public override async Task InitializeAsync()
        {
            try
            {
                _logger.LogDebug("Initializing RabbitMQ 3.x test fixture.");
                
                Amqp.Trace.TraceLevel = Amqp.TraceLevel.Frame;
                Amqp.Trace.TraceListener = (l, f, a) => System.Diagnostics.Debug.WriteLine(f, a);
                
                RabbitMqContainerInstance = new ContainerBuilder()
                    .WithImage("rabbitmq:3-management")
                    .WithPortBinding(5672, true)
                    .WithPortBinding(15672, true)
                    .WithEnvironment("RABBITMQ_DEFAULT_USER", "guest")
                    .WithEnvironment("RABBITMQ_DEFAULT_PASS", "guest")
                    .WithCommand("bash", "-c", "rabbitmq-plugins enable rabbitmq_amqp1_0 && docker-entrypoint.sh rabbitmq-server")
                    .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Server startup complete"))
                    .Build();
                
                await RabbitMqContainerInstance.StartAsync();
                _logger.LogDebug("RabbitMQ 3.x container started.");
                
                var host = RabbitMqContainerInstance.Hostname;
                var port = RabbitMqContainerInstance.GetMappedPublicPort(5672);
                BrokerUrl = new Uri($"amqp://{host}:{port}");
                _logger.LogDebug($"RabbitMQ 3.x broker URL: {BrokerUrl}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "An error occurred during InitializeAsync");
                throw;
            }
        }

        public override async Task DisposeAsync()
        {
            try
            {
                _logger.LogDebug("Disposing RabbitMQ 3.x test fixture.");
                
                if (RabbitMqContainerInstance != null)
                {
                    await RabbitMqContainerInstance.StopAsync();
                    _logger.LogDebug("RabbitMQ 3.x container stopped.");
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "An error occurred during DisposeAsync");
            }
        }
    }

    // Fixture for RabbitMQ 4.0+ (native AMQP 1.0 support)
    public class RabbitMq4Fixture : AmqpBrokerFixture
    {
        public override async Task InitializeAsync()
        {
            try
            {
                _logger.LogDebug("Initializing RabbitMQ 4.0+ test fixture.");
                
                Amqp.Trace.TraceLevel = Amqp.TraceLevel.Frame;
                Amqp.Trace.TraceListener = (l, f, a) => System.Diagnostics.Debug.WriteLine(f, a);
                
                RabbitMqContainerInstance = new ContainerBuilder()
                    .WithImage("rabbitmq:4-management")
                    .WithPortBinding(5672, true)
                    .WithPortBinding(15672, true)
                    .WithEnvironment("RABBITMQ_DEFAULT_USER", "guest")
                    .WithEnvironment("RABBITMQ_DEFAULT_PASS", "guest")
                    .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Server startup complete"))
                    .Build();
                
                await RabbitMqContainerInstance.StartAsync();
                _logger.LogDebug("RabbitMQ 4.0+ container started.");
                
                var host = RabbitMqContainerInstance.Hostname;
                var port = RabbitMqContainerInstance.GetMappedPublicPort(5672);
                BrokerUrl = new Uri($"amqp://{host}:{port}");
                _logger.LogDebug($"RabbitMQ 4.0+ broker URL: {BrokerUrl}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "An error occurred during InitializeAsync");
                throw;
            }
        }

        public override async Task DisposeAsync()
        {
            try
            {
                _logger.LogDebug("Disposing RabbitMQ 4.0+ test fixture.");
                
                if (RabbitMqContainerInstance != null)
                {
                    await RabbitMqContainerInstance.StopAsync();
                    _logger.LogDebug("RabbitMQ 4.0+ container stopped.");
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "An error occurred during DisposeAsync");
            }
        }
    }

    // Collection definitions for xUnit
    [CollectionDefinition("Artemis")]
    public class ArtemisCollectionFixture : ICollectionFixture<ArtemisFixture>
    {
    }

    [CollectionDefinition("RabbitMQ 3")]
    public class RabbitMq3CollectionFixture : ICollectionFixture<RabbitMq3Fixture>
    {
    }

    [CollectionDefinition("RabbitMQ 4")]
    public class RabbitMq4CollectionFixture : ICollectionFixture<RabbitMq4Fixture>
    {
    }
{%- for messagegroupid, messagegroup in messagegroups.items() %}
{%- set test_class_name = (project_name + messagegroupid) |  pascal | strip_dots + "Tests" %}
{%- set producer_class_name = (messagegroupid | pascal | strip_namespace) + "EventProducer" %}
    
    // Test class for Artemis broker
    [Collection("Artemis")]
    public class {{ test_class_name }}WithArtemis 
    {
        private readonly ArtemisFixture _fixture;
        private readonly ILogger _logger;

        public {{ test_class_name }}WithArtemis(ArtemisFixture fixture)
        {
            _fixture = fixture;
            _logger = _fixture.GetLoggerFactory().CreateLogger<{{ test_class_name }}WithArtemis>();
        }

#pragma warning disable CS8604 // init takes care of nullables 

        {%- for messageid, message in messagegroup.messages.items() %}
        {%- set messagename = messageid | strip_namespace | strip_dots | pascal %}
        {%- set message_body_type = util.body_type(data_project_name, root, message) %}
        [Fact]
        public async Task Test{{ messagename }}Message()
        {
            _logger.LogInformation("Starting Test{{ messagename }}Message with Artemis");
            try
            {
                var plainEndpointCredential = new PlainEndpointCredential("guest", "guest");
                var producer = {{ producer_class_name }}.CreateBuilder()
                    .WithEndpoint(new Uri(_fixture.BrokerUrl, _fixture.QueueName))
                    .WithCredential(plainEndpointCredential)
                    .WithNode("/" + _fixture.QueueName)
                    .WithLogger(_fixture.GetLoggerFactory().CreateLogger<{{ producer_class_name }}>())
                    .Build();
                try
                {
                    var messageReceived = new TaskCompletionSource<bool>();
                    var receivedCount = 0;
                    
                    // Set up receiver to verify messages were sent
                    var factory = new ConnectionFactory();
                    var address = new Address(_fixture.BrokerUrl?.AbsoluteUri);
                    var connection = await factory.CreateAsync(address);
                    try 
                    {
                        var session = new Session(connection);
                        try
                        {
                            var receiver = new ReceiverLink(session, "receiver-link", "/" + _fixture.QueueName);
                            try
                            {
                                // Start listening for 5 messages
                                var receiveTask = Task.Run(async () =>
                                {
                                    try
                                    {
                                        for (int i = 0; i < 5; i++)
                                        {
                                            var receivedMessage = await receiver.ReceiveAsync(TimeSpan.FromSeconds(15));
                                            if (receivedMessage != null)
                                            {
                                                receiver.Accept(receivedMessage);
                                                _logger.LogInformation("{{ messagename }} message {i} received", i);
                                                
                                                // Verify CloudEvents properties
                                                Assert.NotNull(receivedMessage.ApplicationProperties);
                                                Assert.Equal("1.0", receivedMessage.ApplicationProperties["cloudEvents:specversion"]);
                                                Assert.Equal("{{ messageid }}", receivedMessage.ApplicationProperties["cloudEvents:type"]);
                                                Assert.NotNull(receivedMessage.ApplicationProperties["cloudEvents:id"]);
                                                Assert.NotNull(receivedMessage.ApplicationProperties["cloudEvents:source"]);
                                                
                                                Interlocked.Increment(ref receivedCount);
                                            }
                                            else
                                            {
                                                _logger.LogWarning("Message {i} not received within timeout", i);
                                                break;
                                            }
                                        }
                                        messageReceived.SetResult(receivedCount == 5);
                                    }
                                    catch (Exception ex)
                                    {
                                        _logger.LogError(ex, "Error receiving messages");
                                        messageReceived.SetException(ex);
                                    }
                                });

                                // Send 5 messages using producer
                                {%- set isCloudEvent = cloudEvents.isCloudEvent(message) %}
                                {%- set is_amqp = amqp.is_amqp(message) %}
                                {%- if isCloudEvent %}
                                {%- set uriargs = cloudEvents.DeclareUriTemplateArguments(message) %}
                                {%- elif is_amqp %}
                                {%- set uriargs = amqp.DeclareUriTemplateArguments(message) %}
                                {%- endif %}
                                for (int i = 0; i < 5; i++)
                                {
                                    {%- if message_body_type != "byte[]" %}
                                    var eventDataTest = new {{ message_body_type }}Tests();
                                    var eventData = eventDataTest.CreateInstance();
                                    await producer.Send{{ messagename }}Async(eventData
                                    {%- if uriargs -%}
                                    {%- for arg in uriargs.split(',') if arg.strip() -%}
                                    , {{ arg.strip().split(' ')[1] -}}: "test"
                                    {%- endfor -%}
                                    {%- endif -%}
                                    );
                                    {%- else %}
                                    var eventData = System.Text.Encoding.UTF8.GetBytes("test data");
                                    await producer.Send{{ messagename }}Async(eventData
                                    {%- if uriargs -%}
                                    {%- for arg in uriargs.split(',') if arg.strip() -%}
                                    , {{ arg.strip().split(' ')[1] -}}: "test"
                                    {%- endfor -%}
                                    {%- endif -%}
                                    );
                                    {%- endif %}
                                }
                                _logger.LogInformation("5 {{ messagename }} messages sent");

                                // Wait for all messages to be received
                                await receiveTask;
                                Assert.True(messageReceived.Task.IsCompleted, "Not all messages were received within the timeout period.");
                                Assert.True(await messageReceived.Task, "Message receive validation failed.");
                                Assert.Equal(5, receivedCount);
                                _logger.LogInformation("All 5 messages sent and received validation completed");
                            }
                            finally
                            {
                                await receiver.CloseAsync();
                            }
                        }
                        finally
                        {
                            await session.CloseAsync();
                        }
                    }
                    finally
                    {
                        await connection.CloseAsync();
                    }
                }
                finally
                {
                    producer.Dispose();
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "An error occurred during Test{{ messagename }}Message");
                throw;
            }
        }
        {%- endfor %}
    }

    // Test class for RabbitMQ 3.x broker
    [Collection("RabbitMQ 3")]
    public class {{ test_class_name }}WithRabbitMq3 
    {
        private readonly RabbitMq3Fixture _fixture;
        private readonly ILogger _logger;

        public {{ test_class_name }}WithRabbitMq3(RabbitMq3Fixture fixture)
        {
            _fixture = fixture;
            _logger = _fixture.GetLoggerFactory().CreateLogger<{{ test_class_name }}WithRabbitMq3>();
        }

#pragma warning disable CS8604 // init takes care of nullables 

        {%- for messageid, message in messagegroup.messages.items() %}
        {%- set messagename = messageid | strip_namespace | strip_dots | pascal %}
        {%- set message_body_type = util.body_type(data_project_name, root, message) %}
        [Fact]
        public async Task Test{{ messagename }}Message()
        {
            _logger.LogInformation("Starting Test{{ messagename }}Message with RabbitMQ 3.x");
            try
            {
                var plainEndpointCredential = new PlainEndpointCredential("guest", "guest");
                var producer = {{ producer_class_name }}.CreateBuilder()
                    .WithEndpoint(new Uri(_fixture.BrokerUrl, _fixture.QueueName))
                    .WithCredential(plainEndpointCredential)
                    .WithLogger(_fixture.GetLoggerFactory().CreateLogger<{{ producer_class_name }}>())
                    .Build();
                try
                {
                    {%- set isCloudEvent = cloudEvents.isCloudEvent(message) %}
                    {%- set is_amqp = amqp.is_amqp(message) %}
                    {%- if isCloudEvent %}
                    {%- set uriargs = cloudEvents.DeclareUriTemplateArguments(message) %}
                    {%- elif is_amqp %}
                    {%- set uriargs = amqp.DeclareUriTemplateArguments(message) %}
                    {%- endif %}
                    {%- if message_body_type != "byte[]" %}
                    var testMessage = new {{ message_body_type }}();
                    {%- else %}
                    var testMessage = Array.Empty<byte>();
                    {%- endif %}
                    await producer.Send{{ messagename }}Async(testMessage
                    {%- if uriargs -%}
                    {%- for arg in uriargs.split(',') if arg.strip() -%}
                    , {{ arg.strip().split(' ')[1] }}: "test"
                    {%- endfor -%}
                    {%- endif -%}
                    );
                    _logger.LogInformation("Message sent successfully");

                    // Verify message was received
                    var address = new Address(_fixture.BrokerUrl?.AbsoluteUri);
                    var connection = await Connection.Factory.CreateAsync(address);
                    try
                    {
                        var session = new Session(connection);
                        try
                        {
                            var receiver = new ReceiverLink(session, "test-receiver", _fixture.QueueName);
                            var receivedMessage = await receiver.ReceiveAsync(TimeSpan.FromSeconds(10));
                            Assert.NotNull(receivedMessage);
                            receiver.Accept(receivedMessage);
                        }
                        finally
                        {
                            await session.CloseAsync();
                        }
                    }
                    finally
                    {
                        await connection.CloseAsync();
                    }
                }
                finally
                {
                    producer.Dispose();
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "An error occurred during Test{{ messagename }}Message");
                throw;
            }
        }
        {%- endfor %}
    }

    // Test class for RabbitMQ 4.0+ broker
    [Collection("RabbitMQ 4")]
    public class {{ test_class_name }}WithRabbitMq4 
    {
        private readonly RabbitMq4Fixture _fixture;
        private readonly ILogger _logger;

        public {{ test_class_name }}WithRabbitMq4(RabbitMq4Fixture fixture)
        {
            _fixture = fixture;
            _logger = _fixture.GetLoggerFactory().CreateLogger<{{ test_class_name }}WithRabbitMq4>();
        }

#pragma warning disable CS8604 // init takes care of nullables 

        {%- for messageid, message in messagegroup.messages.items() %}
        {%- set messagename = messageid | strip_namespace | strip_dots | pascal %}
        {%- set message_body_type = util.body_type(data_project_name, root, message) %}
        [Fact]
        public async Task Test{{ messagename }}Message()
        {
            _logger.LogInformation("Starting Test{{ messagename }}Message with RabbitMQ 4.0+");
            try
            {
                var plainEndpointCredential = new PlainEndpointCredential("guest", "guest");
                var producer = {{ producer_class_name }}.CreateBuilder()
                    .WithEndpoint(new Uri(_fixture.BrokerUrl, _fixture.QueueName))
                    .WithCredential(plainEndpointCredential)
                    .WithLogger(_fixture.GetLoggerFactory().CreateLogger<{{ producer_class_name }}>())
                    .Build();
                try
                {
                    {%- set isCloudEvent = cloudEvents.isCloudEvent(message) %}
                    {%- set is_amqp = amqp.is_amqp(message) %}
                    {%- if isCloudEvent %}
                    {%- set uriargs = cloudEvents.DeclareUriTemplateArguments(message) %}
                    {%- elif is_amqp %}
                    {%- set uriargs = amqp.DeclareUriTemplateArguments(message) %}
                    {%- endif %}
                    {%- if message_body_type != "byte[]" %}
                    var testMessage = new {{ message_body_type }}();
                    {%- else %}
                    var testMessage = Array.Empty<byte>();
                    {%- endif %}
                    await producer.Send{{ messagename }}Async(testMessage
                    {%- if uriargs -%}
                    {%- for arg in uriargs.split(',') if arg.strip() -%}
                    , {{ arg.strip().split(' ')[1] }}: "test"
                    {%- endfor -%}
                    {%- endif -%}
                    );
                    _logger.LogInformation("Message sent successfully");

                    // Verify message was received
                    var address = new Address(_fixture.BrokerUrl?.AbsoluteUri);
                    var connection = await Connection.Factory.CreateAsync(address);
                    try
                    {
                        var session = new Session(connection);
                        try
                        {
                            var receiver = new ReceiverLink(session, "test-receiver", _fixture.QueueName);
                            var receivedMessage = await receiver.ReceiveAsync(TimeSpan.FromSeconds(10));
                            Assert.NotNull(receivedMessage);
                            receiver.Accept(receivedMessage);
                        }
                        finally
                        {
                            await session.CloseAsync();
                        }
                    }
                    finally
                    {
                        await connection.CloseAsync();
                    }
                }
                finally
                {
                    producer.Dispose();
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "An error occurred during Test{{ messagename }}Message");
                throw;
            }
        }
        {%- endfor %}
    }
{%- endfor %}
}
