package rpcserver

import (
	"context"
	"errors"
	"net"
	"testing"
	"time"

	"github.com/go-logr/logr"
	"github.com/go-logr/zapr"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"go.uber.org/zap/zaptest/observer"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	healthpb "google.golang.org/grpc/health/grpc_health_v1"

	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/rpcserver/middlewares"
)

type testHandler struct {
	name     string
	register func(*grpc.Server) error
}

func (h testHandler) Name() string {
	return h.name
}

func (h testHandler) RegisterRPC(server *grpc.Server) error {
	return h.register(server)
}

func newObservedRPCServerLogger() (logr.Logger, *observer.ObservedLogs) {
	core, logs := observer.New(zapcore.Level(-10))
	return zapr.NewLogger(zap.New(core)), logs
}

func TestNewRPCServerRegistersHandlers(t *testing.T) {
	t.Parallel()

	var registeredServers []*grpc.Server
	var registeredHandlers []string
	newHandler := func(name string) RPCHandler {
		return testHandler{
			name: name,
			register: func(server *grpc.Server) error {
				registeredServers = append(registeredServers, server)
				registeredHandlers = append(registeredHandlers, name)
				return nil
			},
		}
	}

	server, err := NewRPCServer(RPCServerParam{
		Handlers: []RPCHandler{
			newHandler("first"),
			newHandler("second"),
		},
	})
	require.NoError(t, err)
	require.Equal(t, []string{"first", "second"}, registeredHandlers)
	require.Len(t, registeredServers, 2)
	require.Same(t, server.server, registeredServers[0])
	require.Same(t, registeredServers[0], registeredServers[1])
}

func TestNewRPCServerReturnsHandlerRegistrationError(t *testing.T) {
	t.Parallel()

	expectedErr := errors.New("registration failed")
	server, err := NewRPCServer(RPCServerParam{
		Handlers: []RPCHandler{
			testHandler{
				name: "failed-handler",
				register: func(*grpc.Server) error {
					return expectedErr
				},
			},
		},
	})

	require.Nil(t, server)
	require.ErrorContains(t, err, "register handler failed-handler failed")
	require.ErrorContains(t, err, expectedErr.Error())
}

func TestNewRPCServerRegistersHealthAndReflectionServices(t *testing.T) {
	t.Parallel()

	server, err := NewRPCServer(RPCServerParam{})
	require.NoError(t, err)

	services := server.server.GetServiceInfo()
	require.Contains(t, services, "grpc.health.v1.Health")
	require.Contains(t, services, "grpc.reflection.v1.ServerReflection")
	require.Contains(t, services, "grpc.reflection.v1alpha.ServerReflection")
}

func TestRPCServerRunAndShutdown(t *testing.T) {
	t.Parallel()

	requestLogger, requestLogs := newObservedRPCServerLogger()
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	require.NoError(t, err)
	address := listener.Addr().String()
	require.NoError(t, listener.Close())

	server, err := NewRPCServer(RPCServerParam{
		Address: address,
		ServerOptions: []grpc.ServerOption{
			(&middlewares.LogMetricMiddlewareBuilder{
				Request: middlewares.RequestLoggerOptions{Logger: requestLogger},
			}).Build(),
		},
	})
	require.NoError(t, err)

	runErrCh := make(chan error, 1)
	go func() {
		runErrCh <- server.Run()
	}()

	dialCtx, cancelDial := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancelDial()
	conn, err := grpc.DialContext(
		dialCtx,
		address,
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithBlock(),
	)
	require.NoError(t, err)
	defer conn.Close()

	checkCtx, cancelCheck := context.WithTimeout(context.Background(), time.Second)
	defer cancelCheck()
	response, err := healthpb.NewHealthClient(conn).Check(checkCtx, &healthpb.HealthCheckRequest{})
	require.NoError(t, err)
	require.Equal(t, healthpb.HealthCheckResponse_SERVING, response.Status)
	require.Len(t, requestLogs.AllUntimed(), 2)
	require.Equal(t,
		[]string{"request started", "request completed"},
		[]string{requestLogs.AllUntimed()[0].Message, requestLogs.AllUntimed()[1].Message},
	)

	shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), time.Second)
	defer cancelShutdown()
	require.NoError(t, server.Shutdown(shutdownCtx))
	require.NoError(t, <-runErrCh)
}

func TestRPCServerShutdownWithCanceledContext(t *testing.T) {
	t.Parallel()

	server, err := NewRPCServer(RPCServerParam{})
	require.NoError(t, err)

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

	require.ErrorIs(t, server.Shutdown(ctx), context.Canceled)

	listener, err := net.Listen("tcp", "127.0.0.1:0")
	require.NoError(t, err)
	require.ErrorIs(t, server.server.Serve(listener), grpc.ErrServerStopped)
}

func freeAddress(t *testing.T) string {
	t.Helper()

	listener, err := net.Listen("tcp", "127.0.0.1:0")
	require.NoError(t, err)
	address := listener.Addr().String()
	require.NoError(t, listener.Close())
	return address
}
