package it

import (
	"context"
	"net"
	"strconv"
	"time"

	"github.com/testcontainers/testcontainers-go"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitaly/vendored/gitalypb"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/mock_gitaly"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/pkg/agentcfg"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/pkg/entity"
	"go.uber.org/mock/gomock"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/keepalive"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"
)

type gitalyHolder struct {
	server  *grpc.Server
	address string
	port    int
}

func (s *integrationSuite) startGitaly() *gitalyHolder {
	l, err := net.Listen("tcp4", ":0")
	s.Require().NoError(err)
	addr := l.Addr().String()
	s.T().Log("Gitaly listening on", addr)
	server := grpc.NewServer(
		grpc.UnknownServiceHandler(func(srv any, stream grpc.ServerStream) error {
			sts := grpc.ServerTransportStreamFromContext(stream.Context())
			s.Fail("Unimplemented Gitaly service", sts.Method())
			return status.Error(codes.Unimplemented, "unimplemented service")
		}),
		// Without this kas sends keepalive pings too often and this fake server disconnects it with a GOAWAY.
		grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
			MinTime:             10 * time.Second, // like in Gitaly
			PermitWithoutStream: true,
		}),
	)
	gitalypb.RegisterCommitServiceServer(server, &commitServiceServer{
		mock: s.gitalyM,
	})
	s.wg.Start(func() {
		s.NoError(server.Serve(l))
	})
	s.cleanup(server.GracefulStop)
	_, port, err := net.SplitHostPort(addr)
	s.Require().NoError(err)
	portInt, err := strconv.ParseUint(port, 10, 16)
	s.Require().NoError(err)
	return &gitalyHolder{
		server: server,
		// See https://github.com/grpc/grpc/blob/master/doc/naming.md.
		address: "dns:" + net.JoinHostPort(testcontainers.HostInternal, port),
		port:    int(portInt),
	}
}

func (s *integrationSuite) setupGitalyMocksForAgentConfig(cfg *agentcfg.ConfigurationFile) {
	cfgBytes, err := protojson.Marshal(cfg)
	s.Require().NoError(err)

	s.gitalyM.EXPECT().
		LastCommitForPath(gomock.Any(), mock_gitaly.LastCommitForPathRequestForAgentConfig(agentName)).
		Return(&gitalypb.LastCommitForPathResponse{
			Commit: &gitalypb.GitCommit{
				Id: "rev1",
			},
		}, nil).
		AnyTimes()
	s.gitalyM.EXPECT().
		TreeEntry(mock_gitaly.TreeEntryRequestForAgentConfig(agentName), gomock.Any()).
		DoAndReturn(func(req *gitalypb.TreeEntryRequest, server gitalypb.CommitService_TreeEntryServer) error {
			return server.Send(&gitalypb.TreeEntryResponse{
				Type: gitalypb.TreeEntryResponse_BLOB,
				Data: cfgBytes,
			})
		}).
		AnyTimes()
}

type commitServiceServer struct {
	gitalypb.UnimplementedCommitServiceServer
	mock GitalyAPI
}

func (s *commitServiceServer) GetTreeEntries(req *gitalypb.GetTreeEntriesRequest, srv gitalypb.CommitService_GetTreeEntriesServer) error {
	return s.mock.GetTreeEntries(req, srv)
}

func (s *commitServiceServer) LastCommitForPath(ctx context.Context, req *gitalypb.LastCommitForPathRequest) (*gitalypb.LastCommitForPathResponse, error) {
	return s.mock.LastCommitForPath(ctx, req)
}

func (s *commitServiceServer) TreeEntry(req *gitalypb.TreeEntryRequest, server gitalypb.CommitService_TreeEntryServer) error {
	return s.mock.TreeEntry(req, server)
}

func gitalyFields(gitalySrv *gitalyHolder) (*entity.GitalyInfo, *entity.GitalyRepository) {
	return &entity.GitalyInfo{
			Address: gitalySrv.address,
			Token:   "do not care",
		},
		&entity.GitalyRepository{
			StorageName:   "storage1",
			RelativePath:  "/a/b/c",
			GlRepository:  "bla/bla",
			GlProjectPath: "blaaaa",
		}
}
