// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package submatview

import (
	"context"
	"testing"
	"time"

	mock "github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/acl/resolver"
	"github.com/hashicorp/consul/agent/consul/state"
	"github.com/hashicorp/consul/agent/consul/stream"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/proto/private/pbsubscribe"
)

func TestLocalMaterializer(t *testing.T) {
	const (
		index = 123
		topic = pbsubscribe.Topic_ServiceResolver
		key   = "web"
		token = "some-acl-token"
	)

	var (
		snapshotEvent = stream.Event{
			Topic: topic,
			Index: index,
			Payload: state.EventPayloadConfigEntry{
				Value: &structs.ServiceResolverConfigEntry{
					Name: key,
					Meta: map[string]string{"snapshot": "true"},
				},
			},
		}

		publishedEvent1 = stream.Event{
			Topic: topic,
			Index: index + 1,
			Payload: state.EventPayloadConfigEntry{
				Value: &structs.ServiceResolverConfigEntry{
					Name: key,
					Meta: map[string]string{"published": "true"},
				},
			},
		}

		publishedEvent2 = stream.Event{
			Topic: topic,
			Index: index + 2,
			Payload: state.EventPayloadConfigEntry{
				Value: &structs.ServiceResolverConfigEntry{
					Name: key,
					Meta: map[string]string{"published": "true"},
				},
			},
		}
	)

	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)

	publisher := stream.NewEventPublisher(10 * time.Second)
	publisher.RegisterHandler(topic, func(req stream.SubscribeRequest, buf stream.SnapshotAppender) (uint64, error) {
		buf.Append([]stream.Event{snapshotEvent})
		return index, nil
	}, false)
	go publisher.Run(ctx)

	// This allows us to swap the authorizer out at runtime.
	authz := &struct{ acl.Authorizer }{acl.AllowAll()}

	aclResolver := NewMockACLResolver(t)
	aclResolver.On("ResolveTokenAndDefaultMeta", token, mock.Anything, mock.Anything).
		Return(resolver.Result{Authorizer: authz}, nil)

	view := newTestView()

	m := NewLocalMaterializer(LocalMaterializerDeps{
		Backend:     publisher,
		ACLResolver: aclResolver,
		Deps: Deps{
			View: view,
			Request: func(index uint64) *pbsubscribe.SubscribeRequest {
				return &pbsubscribe.SubscribeRequest{
					Topic: topic,
					Index: index,
					Subject: &pbsubscribe.SubscribeRequest_NamedSubject{
						NamedSubject: &pbsubscribe.NamedSubject{
							Key: key,
						},
					},
					Token: token,
				}
			},
		},
	})
	go m.Run(ctx)

	// Check that the view received the snapshot events.
	events := view.getEvents(t)
	require.Len(t, events, 1)
	require.Equal(t, snapshotEvent.Payload.ToSubscriptionEvent(index), events[0])

	publisher.Publish([]stream.Event{publishedEvent1})

	// Check that the view received the published events.
	events = view.getEvents(t)
	require.Len(t, events, 1)
	require.Equal(t, publishedEvent1.Payload.ToSubscriptionEvent(index+1), events[0])

	// Replace the authorizer and check that we don't receive newly published events.
	authz.Authorizer = acl.DenyAll()
	publisher.Publish([]stream.Event{publishedEvent2})
	view.expectNoEvents(t)
}

func newTestView() *testView {
	return &testView{
		eventsCh: make(chan []*pbsubscribe.Event),
	}
}

type testView struct {
	eventsCh chan []*pbsubscribe.Event
}

func (testView) Reset() {}

func (testView) Result(uint64) any { return nil }

func (v *testView) Update(events []*pbsubscribe.Event) error {
	v.eventsCh <- events
	return nil
}

func (v *testView) getEvents(t *testing.T) []*pbsubscribe.Event {
	t.Helper()

	select {
	case events := <-v.eventsCh:
		return events
	case <-time.After(100 * time.Millisecond):
		t.Fatal("timeout waiting for view to receive events")
		return nil
	}
}

func (v *testView) expectNoEvents(t *testing.T) {
	t.Helper()

	select {
	case <-v.eventsCh:
		t.Fatal("expected no events to be received")
	case <-time.After(100 * time.Millisecond):
	}
}
