package grpcimp

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/grpc/test/bufconn"
)

const bufconnSize = 1024 * 1024

func newBufferedListener(t *testing.T) *bufconn.Listener {
	t.Helper()

	listener := bufconn.Listen(bufconnSize)
	t.Cleanup(func() {
		require.NoError(t, listener.Close())
	})

	return listener
}

func newTestRouter(t *testing.T, cfg *Config) *Router {
	t.Helper()

	logger := zap.NewNop()
	if cfg == nil {
		cfg = &Config{}
	}

	router, err := NewRouter(logger, cfg, &Options{Listener: newBufferedListener(t)})
	require.NoError(t, err)

	return router
}

func TestRouterStartAndFinish(t *testing.T) {
	router := newTestRouter(t, &Config{})

	doneCh := router.Done()
	require.NotNil(t, doneCh)

	require.NoError(t, router.Register(func(grpc.ServiceRegistrar) {}))

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	require.NoError(t, router.Start(ctx))

	addr := router.Addr()
	require.NotNil(t, addr)
	require.NotEmpty(t, addr.String())

	finishCtx, finishCancel := context.WithTimeout(context.Background(), time.Second)
	defer finishCancel()

	require.NoError(t, router.Finish(finishCtx))

	select {
	case err, ok := <-doneCh:
		if ok {
			require.NoError(t, err)
		}
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for done channel")
	}
}

func TestRouterRejectsRegistrationAfterStart(t *testing.T) {
	router := newTestRouter(t, &Config{})

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	require.NoError(t, router.Start(ctx))

	doneCh := router.Done()

	err := router.Register(func(grpc.ServiceRegistrar) {})
	require.ErrorIs(t, err, errAlreadyStarted)

	finishCtx, finishCancel := context.WithTimeout(context.Background(), time.Second)
	defer finishCancel()

	require.NoError(t, router.Finish(finishCtx))

	select {
	case <-doneCh:
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for done channel")
	}
}

func TestRouterStartOnlyOnce(t *testing.T) {
	router := newTestRouter(t, &Config{})

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	require.NoError(t, router.Start(ctx))
	require.ErrorIs(t, router.Start(ctx), errAlreadyStarted)

	doneCh := router.Done()

	finishCtx, finishCancel := context.WithTimeout(context.Background(), time.Second)
	defer finishCancel()

	require.NoError(t, router.Finish(finishCtx))

	select {
	case <-doneCh:
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for done channel")
	}
}

func TestRouterUsesProvidedListener(t *testing.T) {
	logger := zap.NewNop()
	listener := newBufferedListener(t)

	cfg := &Config{}
	router, err := NewRouter(logger, cfg, &Options{Listener: listener})
	require.NoError(t, err)

	actualListener, ok := router.listener.(*bufconn.Listener)
	require.True(t, ok)
	require.Same(t, listener, actualListener)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	require.NoError(t, router.Start(ctx))

	doneCh := router.Done()

	finishCtx, finishCancel := context.WithTimeout(context.Background(), time.Second)
	defer finishCancel()

	require.NoError(t, router.Finish(finishCtx))

	select {
	case <-doneCh:
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for done channel")
	}
}
