// Go reference: jetstream_cluster.go:7620-8265 — clustered stream/consumer API handlers
// propose to the meta RAFT group rather than applying locally to StreamManager/ConsumerManager.

using System.Text;
using NATS.Server.JetStream.Api.Handlers;
using NATS.Server.JetStream.Cluster;

namespace NATS.Server.JetStream.Tests.JetStream.Api;

public class ClusteredApiTests
{
    // ---------------------------------------------------------------
    // Stream clustered handlers
    // ---------------------------------------------------------------

    /// <summary>
    /// A successful clustered create proposes to the meta group, resulting in a new stream
    /// assignment tracked under the provided name.
    /// Go reference: jetstream_cluster.go:7620 jsClusteredStreamRequest.
    /// </summary>
    [Fact]
    public async Task HandleClusteredCreate_proposes_to_meta_group()
    {
        var metaGroup = new JetStreamMetaGroup(nodes: 1);
        var payload = Encoding.UTF8.GetBytes("""{"name":"ORDERS","subjects":["orders.>"]}""");

        var response = await StreamApiHandlers.HandleClusteredCreateAsync(
            "$JS.API.STREAM.CREATE.ORDERS", payload, metaGroup, CancellationToken.None);

        response.Error.ShouldBeNull();
        response.Success.ShouldBeTrue();
        metaGroup.GetStreamAssignment("ORDERS").ShouldNotBeNull();
    }

    /// <summary>
    /// A duplicate clustered create for the same stream name returns an error response.
    /// Go reference: jetstream_cluster.go — duplicate stream proposal returns error.
    /// </summary>
    [Fact]
    public async Task HandleClusteredCreate_returns_error_for_duplicate()
    {
        var metaGroup = new JetStreamMetaGroup(nodes: 1);
        var payload = Encoding.UTF8.GetBytes("""{"name":"ORDERS","subjects":["orders.>"]}""");

        // First create succeeds.
        var first = await StreamApiHandlers.HandleClusteredCreateAsync(
            "$JS.API.STREAM.CREATE.ORDERS", payload, metaGroup, CancellationToken.None);
        first.Error.ShouldBeNull();

        // Second create for same name returns error.
        var second = await StreamApiHandlers.HandleClusteredCreateAsync(
            "$JS.API.STREAM.CREATE.ORDERS", payload, metaGroup, CancellationToken.None);
        second.Error.ShouldNotBeNull();
        second.Error!.Description.ShouldContain("ORDERS");
    }

    /// <summary>
    /// When this node is not the meta-group leader, clustered create returns a not-leader error.
    /// Go reference: jetstream_cluster.go:7620 — leader check before proposing.
    /// </summary>
    [Fact]
    public async Task HandleClusteredCreate_returns_error_when_not_leader()
    {
        // selfIndex=2, leaderIndex defaults to 1 — this node is NOT the leader.
        var metaGroup = new JetStreamMetaGroup(nodes: 3, selfIndex: 2);
        var payload = Encoding.UTF8.GetBytes("""{"name":"ORDERS","subjects":["orders.>"]}""");

        var response = await StreamApiHandlers.HandleClusteredCreateAsync(
            "$JS.API.STREAM.CREATE.ORDERS", payload, metaGroup, CancellationToken.None);

        response.Error.ShouldNotBeNull();
        response.Error!.Code.ShouldBe(10003);
        response.Error.Description.ShouldBe("not leader");
    }

    /// <summary>
    /// Clustered update proposes a config change to an existing stream assignment.
    /// Go reference: jetstream_cluster.go jsClusteredStreamUpdateRequest.
    /// </summary>
    [Fact]
    public async Task HandleClusteredUpdate_updates_existing_stream()
    {
        var metaGroup = new JetStreamMetaGroup(nodes: 1);

        // Create the stream first.
        var createPayload = Encoding.UTF8.GetBytes("""{"name":"EVENTS","subjects":["events.>"]}""");
        await StreamApiHandlers.HandleClusteredCreateAsync(
            "$JS.API.STREAM.CREATE.EVENTS", createPayload, metaGroup, CancellationToken.None);

        // Now update it with a max_msgs constraint.
        var updatePayload = Encoding.UTF8.GetBytes("""{"name":"EVENTS","subjects":["events.>"],"max_msgs":500}""");
        var response = await StreamApiHandlers.HandleClusteredUpdateAsync(
            "$JS.API.STREAM.UPDATE.EVENTS", updatePayload, metaGroup, CancellationToken.None);

        response.Error.ShouldBeNull();
        response.Success.ShouldBeTrue();

        // The assignment should still exist.
        metaGroup.GetStreamAssignment("EVENTS").ShouldNotBeNull();
    }

    /// <summary>
    /// Clustered delete proposes removal of a stream from the meta group.
    /// Go reference: jetstream_cluster.go processStreamRemoval via meta leader.
    /// </summary>
    [Fact]
    public async Task HandleClusteredDelete_proposes_deletion()
    {
        var metaGroup = new JetStreamMetaGroup(nodes: 1);
        var createPayload = Encoding.UTF8.GetBytes("""{"name":"ORDERS","subjects":["orders.>"]}""");
        await StreamApiHandlers.HandleClusteredCreateAsync(
            "$JS.API.STREAM.CREATE.ORDERS", createPayload, metaGroup, CancellationToken.None);

        metaGroup.GetStreamAssignment("ORDERS").ShouldNotBeNull();

        var response = await StreamApiHandlers.HandleClusteredDeleteAsync(
            "$JS.API.STREAM.DELETE.ORDERS", metaGroup, CancellationToken.None);

        response.Error.ShouldBeNull();
        response.Success.ShouldBeTrue();
        metaGroup.GetStreamAssignment("ORDERS").ShouldBeNull();
    }

    /// <summary>
    /// Clustered delete of a non-existent stream returns a 404 not-found error.
    /// Go reference: jetstream_cluster.go — delete missing stream returns error.
    /// </summary>
    [Fact]
    public async Task HandleClusteredDelete_returns_error_for_missing_stream()
    {
        var metaGroup = new JetStreamMetaGroup(nodes: 1);

        var response = await StreamApiHandlers.HandleClusteredDeleteAsync(
            "$JS.API.STREAM.DELETE.GHOST", metaGroup, CancellationToken.None);

        response.Error.ShouldNotBeNull();
        response.Error!.Code.ShouldBe(404);
    }

    // ---------------------------------------------------------------
    // Consumer clustered handlers
    // ---------------------------------------------------------------

    /// <summary>
    /// Clustered consumer create proposes to the meta group, adding the consumer to the
    /// stream's assignment map.
    /// Go reference: jetstream_cluster.go:8100 jsClusteredConsumerRequest.
    /// </summary>
    [Fact]
    public async Task Consumer_clustered_create_proposes_to_meta()
    {
        var metaGroup = new JetStreamMetaGroup(nodes: 1);

        // Create parent stream first.
        await StreamApiHandlers.HandleClusteredCreateAsync(
            "$JS.API.STREAM.CREATE.ORDERS",
            Encoding.UTF8.GetBytes("""{"name":"ORDERS","subjects":["orders.>"]}"""),
            metaGroup,
            CancellationToken.None);

        var consumerPayload = Encoding.UTF8.GetBytes("""{"durable_name":"MON","filter_subject":"orders.created"}""");
        var response = await ConsumerApiHandlers.HandleClusteredCreateAsync(
            "$JS.API.CONSUMER.CREATE.ORDERS.MON", consumerPayload, metaGroup, CancellationToken.None);

        response.Error.ShouldBeNull();
        response.Success.ShouldBeTrue();
        metaGroup.GetConsumerAssignment("ORDERS", "MON").ShouldNotBeNull();
    }

    /// <summary>
    /// Creating a consumer on a stream that does not exist in the meta group returns an error.
    /// Go reference: jetstream_cluster.go — consumer proposal validates stream existence.
    /// </summary>
    [Fact]
    public async Task Consumer_clustered_create_returns_error_for_missing_stream()
    {
        var metaGroup = new JetStreamMetaGroup(nodes: 1);
        var payload = Encoding.UTF8.GetBytes("""{"durable_name":"MON","filter_subject":"orders.created"}""");

        var response = await ConsumerApiHandlers.HandleClusteredCreateAsync(
            "$JS.API.CONSUMER.CREATE.GHOST.MON", payload, metaGroup, CancellationToken.None);

        response.Error.ShouldNotBeNull();
        response.Error!.Description.ShouldContain("GHOST");
    }

    /// <summary>
    /// Clustered consumer delete removes the consumer from the stream assignment.
    /// Go reference: jetstream_cluster.go processConsumerRemoval via meta leader.
    /// </summary>
    [Fact]
    public async Task Consumer_clustered_delete_removes_consumer()
    {
        var metaGroup = new JetStreamMetaGroup(nodes: 1);

        // Set up stream and consumer.
        await StreamApiHandlers.HandleClusteredCreateAsync(
            "$JS.API.STREAM.CREATE.ORDERS",
            Encoding.UTF8.GetBytes("""{"name":"ORDERS","subjects":["orders.>"]}"""),
            metaGroup,
            CancellationToken.None);

        await ConsumerApiHandlers.HandleClusteredCreateAsync(
            "$JS.API.CONSUMER.CREATE.ORDERS.MON",
            Encoding.UTF8.GetBytes("""{"durable_name":"MON"}"""),
            metaGroup,
            CancellationToken.None);

        metaGroup.GetConsumerAssignment("ORDERS", "MON").ShouldNotBeNull();

        var response = await ConsumerApiHandlers.HandleClusteredDeleteAsync(
            "$JS.API.CONSUMER.DELETE.ORDERS.MON", metaGroup, CancellationToken.None);

        response.Error.ShouldBeNull();
        response.Success.ShouldBeTrue();
        metaGroup.GetConsumerAssignment("ORDERS", "MON").ShouldBeNull();
    }

    /// <summary>
    /// Deleting a non-existent consumer returns a 404 not-found error.
    /// Go reference: jetstream_cluster.go — consumer delete validates existence.
    /// </summary>
    [Fact]
    public async Task Consumer_clustered_delete_returns_not_found_for_missing()
    {
        var metaGroup = new JetStreamMetaGroup(nodes: 1);

        // Create the stream but not the consumer.
        await StreamApiHandlers.HandleClusteredCreateAsync(
            "$JS.API.STREAM.CREATE.ORDERS",
            Encoding.UTF8.GetBytes("""{"name":"ORDERS","subjects":["orders.>"]}"""),
            metaGroup,
            CancellationToken.None);

        var response = await ConsumerApiHandlers.HandleClusteredDeleteAsync(
            "$JS.API.CONSUMER.DELETE.ORDERS.GHOST", metaGroup, CancellationToken.None);

        response.Error.ShouldNotBeNull();
        response.Error!.Code.ShouldBe(404);
    }
}
