package it

import (
	"time"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitaly/vendored/gitalypb"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/managed_resources"
	managed_resources_rpc "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/managed_resources/rpc"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/testing/matcher"
	"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"
	"go.uber.org/mock/gomock"
	"google.golang.org/grpc"
	"k8s.io/utils/ptr"
)

const (
	overlappingEnvTemplate = `objects:
- apiVersion: v1
  kind: Namespace
  metadata:
    name: '{{ .environment.slug }}' # per env namespace
- apiVersion: v1
  kind: Namespace
  metadata:
    name: '{{ .agent.id }}' # all environments of the agent will use the same namespace
- apiVersion: rbac.authorization.k8s.io/v1
  kind: RoleBinding
  metadata:
    name: bind-ci-job-{{ .agent.id }}-fixed
    namespace: '{{ .agent.id }}'
  roleRef:
    apiGroup: ""
    kind: ClusterRole
    name: admin
  subjects:
  - kind: Group
    name: gitlab:project_env:fixed
- apiVersion: rbac.authorization.k8s.io/v1
  kind: RoleBinding
  metadata:
    name: bind-ci-job-{{ .environment.slug }}
    namespace: '{{ .environment.slug }}'
  roleRef:
    apiGroup: ""
    kind: ClusterRole
    name: admin
  subjects:
  - kind: Group
    name: gitlab:project_env:{{ .project.id }}:{{ .environment.slug }}
- apiVersion: rbac.authorization.k8s.io/v1
  kind: RoleBinding
  metadata:
    name: bind-ci-job-{{ .environment.slug }}
    namespace: my-fixed-ns # fixed namespace
  roleRef:
    apiGroup: ""
    kind: ClusterRole
    name: admin
  subjects:
  - kind: Group
    name: gitlab:project_env:{{ .project.id }}:{{ .environment.slug }}
`
)

func (s *integrationSuite) TestManagedResources() {
	gitalySrv := s.startGitaly()
	s.setupGitLabMocks(gitalySrv, true)
	s.setupGitalyMocksForAgentConfig(&agentcfg.ConfigurationFile{
		Observability: &agentcfg.ObservabilityCF{
			Logging: &agentcfg.LoggingCF{
				Level:     agentcfg.LogLevelEnum_debug,
				GrpcLevel: ptr.To(agentcfg.LogLevelEnum_debug),
			},
		},
	})
	gitLabSrv := s.startGitLab()

	kas := s.startKAS(kasOptions{
		name:       "kas",
		cfg:        s.baseKASConfig(gitLabSrv),
		gitalyPort: gitalySrv.port,
	})
	s.startAgentk(agentkOptions{
		kasURL: kas.agentServerURL,
		token:  agentkToken1,
		name:   "agent1",
		ns:     s.ns,
	})

	kasAPI := s.clientForKASAPI(kas)
	c := managed_resources_rpc.NewProvisionerClient(kasAPI)
	info := templatingInfo()
	gitalyInfo, gitalyRepository := gitalyFields(gitalySrv)

	s.Run("Get default template, render, 2x ensure, delete", func() {
		tpl, err := c.GetDefaultEnvironmentTemplate(s.ctx, &managed_resources_rpc.GetDefaultEnvironmentTemplateRequest{})
		s.Require().NoError(err)
		s.T().Log("Template is:\n", string(tpl.Template.Data))

		renderedTpl, err := c.RenderEnvironmentTemplate(s.ctx, &managed_resources_rpc.RenderEnvironmentTemplateRequest{
			Template: tpl.Template,
			Info:     info,
		})
		s.Require().NoError(err)
		s.T().Log("Rendered template is:\n", string(renderedTpl.Template.Data))

		envResp, err := c.EnsureEnvironment(s.ctx, &managed_resources_rpc.EnsureEnvironmentRequest{
			Template: renderedTpl.Template,
			Info:     info,
		})
		s.Require().NoError(err)

		s.assertObjectsBelongToEnvs(envResp.Objects, info)
		s.Require().Empty(envResp.Errors)

		envResp, err = c.EnsureEnvironment(s.ctx, &managed_resources_rpc.EnsureEnvironmentRequest{
			Template: renderedTpl.Template,
			Info:     info,
		})
		s.Require().NoError(err)
		s.assertObjectsBelongToEnvs(envResp.Objects, info)
		s.Require().Empty(envResp.Errors)
		s.deleteEnvironment(c, info.Agent.Id, info.Project.Id, info.Environment.Slug, envResp.Objects)
		s.assertObjectsDoNotExist(envResp.Objects...)
	})
	s.Run("Ensure multiple overlapping environments, delete", func() {
		s.createNS("my-fixed-ns", "l1") // matches the object namespace in the template

		info1 := templatingInfo()
		envResp1 := s.renderAndEnsure(c, info1)
		s.assertObjectsBelongToEnvs(envResp1.Objects, info1)

		info2 := templatingInfo()
		envResp2 := s.renderAndEnsure(c, info2)
		s.assertObjectsBelongToEnvs(envResp2.Objects, info2)

		s.assertObjectsBelongToEnvs(envResp1.Objects, info1) // still belong to environment1 (not modified)

		s.T().Log("Deleting environment 2", info2.Environment.Slug)
		s.deleteEnvironment(c, info2.Agent.Id, info2.Project.Id, info2.Environment.Slug, envResp2.Objects)

		s.assertObjectsBelongToEnvs(envResp1.Objects, info1) // environment1 objects should still all be there

		objs1 := rpcObj2setOfObjs(envResp1.Objects)
		objs2 := rpcObj2setOfObjs(envResp2.Objects)
		s.assertObjectsDoNotExist(setOfObjs2rpcObj(objs2.Difference(objs1))...) // objects that were part of environment2 only should be gone

		s.T().Log("Deleting environment 1", info1.Environment.Slug)
		s.deleteEnvironment(c, info1.Agent.Id, info1.Project.Id, info1.Environment.Slug, envResp1.Objects)

		s.assertObjectsDoNotExist(envResp1.Objects...)
		s.assertObjectsDoNotExist(envResp2.Objects...)
	})
	s.Run("ListEnvironmentTemplates for all agents", func() {
		s.gitalyM.EXPECT().
			GetTreeEntries(mock_gitaly.GetTreeEntriesRequestForAllAgents(), gomock.Any()).
			DoAndReturn(func(r *gitalypb.GetTreeEntriesRequest, srv grpc.ServerStreamingServer[gitalypb.GetTreeEntriesResponse]) error {
				return srv.Send(&gitalypb.GetTreeEntriesResponse{
					Entries: []*gitalypb.TreeEntry{
						{
							Path: []byte(".gitlab/agents/agent1/environment_templates/name1.yaml"),
							Type: gitalypb.TreeEntry_BLOB,
						},
						{
							Path: []byte(".gitlab/agents/agent2/environment_templates/name2.yaml"),
							Type: gitalypb.TreeEntry_BLOB,
						},
					},
				})
			})
		resp, err := c.ListEnvironmentTemplates(s.ctx, &managed_resources_rpc.ListEnvironmentTemplatesRequest{
			AgentName:        nil, // all agents
			GitalyInfo:       gitalyInfo,
			GitalyRepository: gitalyRepository,
			DefaultBranch:    "main",
		})
		s.Require().NoError(err)

		s.Require().Len(resp.Infos, 2)
		matcher.AssertProtoEqual(s.T(), resp.Infos[0], &managed_resources_rpc.EnvironmentTemplateInfo{AgentName: "agent1", TemplateName: "name1"})
		matcher.AssertProtoEqual(s.T(), resp.Infos[1], &managed_resources_rpc.EnvironmentTemplateInfo{AgentName: "agent2", TemplateName: "name2"})
	})
	s.Run("ListEnvironmentTemplates for a particular agent", func() {
		s.gitalyM.EXPECT().
			GetTreeEntries(mock_gitaly.GetTreeEntriesRequestForAnAgent(agentName), gomock.Any()).
			DoAndReturn(func(r *gitalypb.GetTreeEntriesRequest, srv grpc.ServerStreamingServer[gitalypb.GetTreeEntriesResponse]) error {
				return srv.Send(&gitalypb.GetTreeEntriesResponse{
					Entries: []*gitalypb.TreeEntry{
						{
							Path: []byte(".gitlab/agents/" + agentName + "/environment_templates/name1.yaml"),
							Type: gitalypb.TreeEntry_BLOB,
						},
					},
				})
			})
		resp, err := c.ListEnvironmentTemplates(s.ctx, &managed_resources_rpc.ListEnvironmentTemplatesRequest{
			AgentName:        ptr.To(agentName),
			GitalyInfo:       gitalyInfo,
			GitalyRepository: gitalyRepository,
			DefaultBranch:    "main",
		})
		s.Require().NoError(err)

		s.Require().Len(resp.Infos, 1)
		matcher.AssertProtoEqual(s.T(), resp.Infos[0], &managed_resources_rpc.EnvironmentTemplateInfo{AgentName: agentName, TemplateName: "name1"})
	})
	s.Run("Get a template for an agent", func() {
		t1Data := []byte("template: n11")
		s.setupTreeEntryMockForTemplate("agent1", "name1", t1Data)
		resp, err := c.GetEnvironmentTemplate(s.ctx, &managed_resources_rpc.GetEnvironmentTemplateRequest{
			TemplateName:     "name1",
			AgentName:        "agent1",
			GitalyInfo:       gitalyInfo,
			GitalyRepository: gitalyRepository,
			DefaultBranch:    "main",
		})
		s.Require().NoError(err)
		matcher.AssertProtoEqual(s.T(), resp.Template, &managed_resources.EnvironmentTemplate{Name: "name1", Data: t1Data})
	})
}

func (s *integrationSuite) renderAndEnsure(c managed_resources_rpc.ProvisionerClient, info *managed_resources.TemplatingInfo) *managed_resources_rpc.EnsureEnvironmentResponse {
	renderedTpl, err := c.RenderEnvironmentTemplate(s.ctx, &managed_resources_rpc.RenderEnvironmentTemplateRequest{
		Template: &managed_resources.EnvironmentTemplate{
			Name: "test-tpl",
			Data: []byte(overlappingEnvTemplate),
		},
		Info: info,
	})
	s.Require().NoError(err)
	s.T().Logf("Rendered template for env id=%d is:\n%s", info.Environment.Id, string(renderedTpl.Template.Data))

	envResp, err := c.EnsureEnvironment(s.ctx, &managed_resources_rpc.EnsureEnvironmentRequest{
		Template: renderedTpl.Template,
		Info:     info,
	})
	s.Require().NoError(err)
	s.Require().Empty(envResp.Errors)

	return envResp
}

func (s *integrationSuite) deleteEnvironment(c managed_resources_rpc.ProvisionerClient, agentID, projectID int64, environmentSlug string, objs []*managed_resources_rpc.Object) {
	objsToDelete := objs
	s.Eventually(func() bool {
		delResp, err := c.DeleteEnvironment(s.ctx, &managed_resources_rpc.DeleteEnvironmentRequest{
			AgentId:         agentID,
			ProjectId:       projectID,
			EnvironmentSlug: environmentSlug,
			Objects:         objsToDelete,
		})
		s.Require().NoError(err)
		s.Require().Empty(delResp.Errors)
		objsToDelete = delResp.InProgress
		l := len(objsToDelete)
		if l > 0 {
			s.T().Logf("%d object(s) are being deleted. Sleeping and trying again", l)
		}
		return l == 0
	}, time.Minute, time.Second)
}

func (s *integrationSuite) setupTreeEntryMockForTemplate(agentName, templateName string, data []byte) {
	s.gitalyM.EXPECT().
		TreeEntry(mock_gitaly.TreeEntryRequestForATemplate(agentName, templateName), gomock.Any()).
		DoAndReturn(func(r *gitalypb.TreeEntryRequest, srv grpc.ServerStreamingServer[gitalypb.TreeEntryResponse]) error {
			return srv.Send(&gitalypb.TreeEntryResponse{
				Type: gitalypb.TreeEntryResponse_BLOB,
				Size: int64(len(data)),
				Data: data,
			})
		})
}
